import pygame
import random
import math
import sys
import time
import numpy as np
import csv
import os
from datetime import datetime
from scipy.stats import norm

import os

class _BufferedOSRNG:
    def __init__(self, bufsize=4096):
        self.bufsize = bufsize
        self.buf = b""
        self.i = 0

    def bit(self):
        if self.i >= len(self.buf) * 8:
            self.buf = os.urandom(self.bufsize)
            self.i = 0
        byte_index = self.i // 8
        bit_index  = self.i % 8
        self.i += 1
        return (self.buf[byte_index] >> bit_index) & 1

_osrng = _BufferedOSRNG(bufsize=4096)

# --- Configuration ---
# Window settings
WIDTH, HEIGHT = 800, 600
TITLE = "Psychokinesis Interface"
FPS = 60  # Screen refresh rate

# File settings
RESULTS_FILE = "psi_test_results.csv"

# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (200, 200, 200)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
VIOLET = (148, 0, 211)

# Test settings
TEST_DURATION = 60  # seconds
UPDATES_PER_SECOND = 10 # Hz for needle movement

# --- Main Application Class ---
class PsiApp:
    def __init__(self):
        """Initialize the application, pygame, and set up the window."""
        pygame.mixer.pre_init(44100, -16, 2, 512) 
        pygame.init()
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption(TITLE)
        self.clock = pygame.time.Clock()
        self.font = pygame.font.SysFont("Arial", 30)
        self.small_font = pygame.font.SysFont("Arial", 22)
        self.instruction_font = pygame.font.SysFont("Arial", 24, bold=True)
        self.state = "IDLE" 
        self.bell_curve_surface = self.create_bell_curve_gradient()
        self.chime_sound = self.generate_chime_sound()

    def get_true_random_move(self):
        # 0 -> -1, 1 -> +1
        return 1 if _osrng.bit() else -1

    def generate_chime_sound(self):
        """Generates a simple, pleasant chime sound in stereo."""
        sample_rate = 44100
        duration = 0.5
        frequency = 880.0
        n_samples = int(round(duration * sample_rate))
        mono_wave = np.zeros(n_samples, dtype=np.int16)
        amplitude = 2**12

        for i in range(n_samples):
            t = float(i) / sample_rate
            sine_val = np.sin(2 * np.pi * frequency * t)
            fade_out = max(0, 1 - (t / duration))
            mono_wave[i] = int(amplitude * sine_val * fade_out)
            
        stereo_wave = np.zeros((n_samples, 2), dtype=np.int16)
        stereo_wave[:, 0] = mono_wave
        stereo_wave[:, 1] = mono_wave
        
        return pygame.sndarray.make_sound(stereo_wave)

    def reset_test(self):
        """Reset all variables to start a new test."""
        self.start_time = 0
        self.last_update_time = 0
        self.last_score_update_time = 0
        self.elapsed_time = 0
        self.current_pos = 0
        self.total_steps = TEST_DURATION * UPDATES_PER_SECOND
        self.max_pos = self.total_steps
        self.score = 0
        self.final_stats = None
        sd_of_distribution = math.sqrt(self.total_steps)
        self.score_scaling_factor = 3 * sd_of_distribution

    def create_bell_curve_gradient(self):
        """Pre-renders the bell curve with a color gradient onto a surface."""
        surface = pygame.Surface((WIDTH, HEIGHT))
        surface.fill(WHITE)
        center_x = WIDTH // 2
        bell_std_dev = WIDTH / 6 
        for x in range(WIDTH):
            distance_from_center = abs(x - center_x)
            ratio = min(distance_from_center / (WIDTH / 2), 1.0)
            if ratio < 0.5:
                r = RED[0] * (1 - ratio * 2) + BLUE[0] * (ratio * 2)
                g = RED[1] * (1 - ratio * 2) + BLUE[1] * (ratio * 2)
                b = RED[2] * (1 - ratio * 2) + BLUE[2] * (ratio * 2)
            else:
                local_ratio = (ratio - 0.5) * 2
                r = BLUE[0] * (1 - local_ratio) + VIOLET[0] * local_ratio
                g = BLUE[1] * (1 - local_ratio) + VIOLET[1] * local_ratio
                b = BLUE[2] * (1 - local_ratio) + VIOLET[2] * local_ratio
            color = (r, g, b)
            exponent = -((x - center_x) ** 2) / (2 * bell_std_dev ** 2)
            y_height = int((HEIGHT * 0.8) * math.exp(exponent))
            pygame.draw.line(surface, color, (x, HEIGHT), (x, HEIGHT - y_height))
        return surface

    def calculate_and_save_statistics(self):
        """Calculate, format, and save the final statistics."""
        if self.current_pos == 0:
            std_devs_from_mean = 0.0
            odds_str = "1 in 1"
        else:
            sd_of_distribution = math.sqrt(self.total_steps)
            std_devs_from_mean = abs(self.current_pos) / sd_of_distribution
            p_value = norm.sf(std_devs_from_mean) * 2
            if p_value < 1e-12:
                odds_str = "Extremely high"
            else:
                odds = 1 / p_value
                odds_str = f"1 in {odds:,.1f}"
        
        self.final_stats = { "std_devs": std_devs_from_mean, "odds": odds_str }
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        data_row = [
            timestamp, 
            f"{self.score:.2f}", 
            f"{self.final_stats['std_devs']:.2f}", 
            self.final_stats['odds']
        ]
        
        file_exists = os.path.isfile(RESULTS_FILE)
        try:
            with open(RESULTS_FILE, 'a', newline='') as csvfile:
                writer = csv.writer(csvfile)
                if not file_exists:
                    writer.writerow(["Timestamp", "Score", "StandardDeviations", "Odds"])
                writer.writerow(data_row)
        except IOError as e:
            print(f"Error saving results: {e}")

    def update_score(self):
        """Calculates the score based on the new statistical scaling."""
        if self.score_scaling_factor > 0:
            self.score = (abs(self.current_pos) / self.score_scaling_factor) * 100
            self.score = min(100, self.score)
        else:
            self.score = 0

    def run(self):
        """The main application loop."""
        self.reset_test()
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                if event.type == pygame.MOUSEBUTTONDOWN:
                    if self.state in ["IDLE", "FINISHED"]:
                        if self.start_button_rect.collidepoint(event.pos):
                            self.state = "RUNNING"
                            self.reset_test()
                            self.start_time = time.time()
                            self.last_update_time = self.start_time
                            self.last_score_update_time = self.start_time

            current_time = time.time()

            if self.state == "RUNNING":
                self.elapsed_time = current_time - self.start_time
                if self.elapsed_time >= TEST_DURATION:
                    self.elapsed_time = TEST_DURATION
                    self.state = "FINISHED"
                    self.update_score()
                    self.calculate_and_save_statistics()
                    self.chime_sound.play()
                else:
                    if current_time - self.last_update_time >= 1.0 / UPDATES_PER_SECOND:
                        self.last_update_time = current_time
                        # --- CHANGE: Use the new true random function ---
                        self.current_pos += self.get_true_random_move()
                        
                    if current_time - self.last_score_update_time >= 1.0:
                        self.last_score_update_time = current_time
                        self.update_score()

            self.screen.blit(self.bell_curve_surface, (0, 0))
            needle_x = WIDTH / 2 + (self.current_pos / self.max_pos) * (WIDTH / 2)
            pygame.draw.line(self.screen, BLACK, (needle_x, 50), (needle_x, HEIGHT - 50), 3)

            remaining_time = max(0, TEST_DURATION - self.elapsed_time)
            timer_text = self.font.render(f"Time Left: {remaining_time:.1f}s", True, BLACK)
            self.screen.blit(timer_text, (10, 10))
            score_text = self.font.render(f"Score: {self.score:.1f}", True, BLACK)
            score_rect = score_text.get_rect(topright=(WIDTH - 10, 10))
            self.screen.blit(score_text, score_rect)

            if self.state == "IDLE":
                self.draw_instructions()
                self.draw_button("Start")
            elif self.state == "FINISHED":
                self.draw_button("Run Again")
                self.draw_results()

            pygame.display.flip()
            self.clock.tick(FPS)

        pygame.quit()
        sys.exit()

    def draw_instructions(self):
        """Draws the initial instructions for the user."""
        line1 = self.instruction_font.render("The Goal: Use your mental focus to influence the random numbers.", True, BLACK)
        line2 = self.instruction_font.render("Push the black line as far to the left or right as possible.", True, BLACK)
        line1_rect = line1.get_rect(center=(WIDTH / 2, HEIGHT / 2 - 80))
        line2_rect = line2.get_rect(center=(WIDTH / 2, HEIGHT / 2 - 50))
        self.screen.blit(line1, line1_rect)
        self.screen.blit(line2, line2_rect)

    def draw_button(self, text):
        """Draws the start/run again button."""
        button_text = self.font.render(text, True, BLACK)
        self.start_button_rect = pygame.Rect((WIDTH / 2 - 100, HEIGHT / 2 - 25, 200, 50))
        pygame.draw.rect(self.screen, GRAY, self.start_button_rect)
        self.screen.blit(button_text, (self.start_button_rect.centerx - button_text.get_width() / 2,
                                       self.start_button_rect.centery - button_text.get_height() / 2))

    def draw_results(self):
        """Draws the final statistics with a text shadow for readability."""
        if self.final_stats:
            lines_info = [
                (self.font, "Test Complete"),
                (self.small_font, f"Final Score: {self.score:.2f}"),
                (self.small_font, f"Result: {self.final_stats['std_devs']:.2f} Standard Deviations from Center"),
                (self.small_font, f"Chance of this result: {self.final_stats['odds']}")
            ]
            start_y = 120
            line_spacing = 40
            for i, (font, text) in enumerate(lines_info):
                shadow_surf = font.render(text, True, BLACK)
                text_surf = font.render(text, True, WHITE)
                x_pos = WIDTH / 2 - text_surf.get_width() / 2
                y_pos = start_y + i * line_spacing
                self.screen.blit(shadow_surf, (x_pos + 2, y_pos + 2))
                self.screen.blit(text_surf, (x_pos, y_pos))

# --- Run the App ---
if __name__ == "__main__":
    app = PsiApp()
    app.run()